﻿using System;
using System.Collections.Generic;
using System.Text;

namespace Entropy.Ext.Ast
{
    public abstract class Node
    {
		internal void FindAndReplace<T>(Func<T, bool> selector, Func<T, Node> replace) where T: Node
		{
			FindAndReplaceIn(this, n => (n is T) && selector((T)n), n => replace((T)n));
		}
		internal void FindAndReplace(Func<Node, bool> selector, Func<Node, Node> replace)
		{
			FindAndReplaceIn(this, selector, replace);
		}

	    private static void FindAndReplaceIn(object me, Func<Node, bool> selector, Func<Node, Node> replace)
	    {
			foreach (var field in me.GetType().GetFields())
		    {
			    if (IsNode(field.FieldType))
			    {
					var value = (Node)field.GetValue(me);
				    if (value != null)
				    {
					    FindAndReplaceIn(value, selector, replace);
					    if (selector(value))
						    field.SetValue(me, replace(value));
				    }
			    }
				else if (field.FieldType.IsArray)
			    {
					var array = (Array)field.GetValue(me);
				    for (int i = 0; i < array.Length; i++)
				    {
						var value = array.GetValue(i);
					    if (value != null)
					    {
						    FindAndReplaceIn(value, selector, replace);
						    var node = value as Node;
						    if (node != null && selector(node))
							    array.SetValue(replace(node), i);
					    }
				    }
			    }
				else if (field.FieldType.IsClass && field.FieldType != typeof(string))
			    {
				    var value = field.GetValue(me);
				    if (value != null)
						FindAndReplaceIn(value, selector, replace);
			    }
		    }
	    }

	    internal IEnumerable<Node> Descendants()
		{
			foreach (var field in GetType().GetFields())
			{
				if (IsNode(field.FieldType))
				{
					yield return (Node)field.GetValue(this);
				}
				else if (field.FieldType.IsArray && IsNode(field.FieldType.GetElementType()))
				{
					var array = (Array)field.GetValue(this);
					for (int i = 0; i < array.Length; i++)
					{
						yield return (Node)array.GetValue(i);
					}
				}
			}
		}

	    public string Dump
	    {
		    get
		    {
				var sb = new StringBuilder(); 
				DoDump(sb, "");
			    return sb.ToString();
		    }
	    }

	    private void DoDump(StringBuilder sb, string offset)
	    {
		    
		    sb.AppendLine(GetType().ToString());
		    sb.AppendLine(offset + "{");
			offset += "    ";
		    

			foreach (var field in GetType().GetFields())
			{
				sb.Append(offset + field.Name + ": ");
				if (IsNode(field.FieldType))
				{
					((Node)field.GetValue(this)).DoDump(sb, offset);
				}
				else if (field.FieldType.IsArray && IsNode(field.FieldType.GetElementType()))
				{
					sb.AppendLine("[");
					offset += "    ";
					var array = (Array) field.GetValue(this);
					for (int i = 0; i < array.Length; i++)
					{
						sb.Append(offset);
						((Node) array.GetValue(i)).DoDump(sb, offset);
					}
					offset = offset.Remove(offset.Length - 4, 4);
					sb.AppendLine(offset + "]");
				}
				else
				{
					sb.AppendLine(field.GetValue(this).ToString());
				}
			}
			offset = offset.Remove(offset.Length - 4, 4);
			sb.AppendLine(offset + "}");
	    }

	    private static bool IsNode(Type type)
	    {
		    return typeof(Node).IsAssignableFrom(type);
	    }
    }
}